/**
 *    Copyright 2015-2018 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.happy.cat.framework.redis;

import com.alibaba.fastjson.JSON;
import com.happy.cat.common.utils.spring.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheKey;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.function.Function;

/**
 * Cache adapter for Redis.
 *
 * @author Eduardo Macarron
 */
@Slf4j
public final class MybatisRedisCache implements Cache {

	private final ReadWriteLock readWriteLock = new DummyReadWriteLock();

	private final String id;

	private RedisTemplate<Object, Object> redisTemplate;

	private Integer timeout = 3600 * 6;

	private final String memoryId=UUID.randomUUID().toString();

	private final String topicId;

	private final Executor<RedisTemplate<Object, Object>> redisExecutor=new Executor<RedisTemplate<Object, Object>>() {

		@Override
        RedisTemplate<Object, Object> get() {
			if (redisTemplate != null) {
				return redisTemplate;
			}
			synchronized (this) {
				if (redisTemplate != null) {
					return redisTemplate;
				}
				if (SpringUtils.getApplicationContext() != null) {
					RedisConnectionFactory connectionFactory= SpringUtils.getBean(RedisConnectionFactory.class);
					RedisTemplate<Object, Object> template=new RedisTemplate<>();
					template.setConnectionFactory(connectionFactory);
					template.afterPropertiesSet();
					addMessageListener(SpringUtils.getBean(RedisMessageListenerContainer.class));
					return redisTemplate = template;
				}
			}
			if(redisTemplate==null) {
				log.warn("redisTemplate is null");
			}
			return redisTemplate;
		}

	};

	private void addMessageListener(RedisMessageListenerContainer container) {
		container.addMessageListener(new MessageListenerAdapter() {

			@Override
			public void onMessage(Message message, byte[] pattern) {
				log.debug("message:{} {}",message,pattern);
			}

		}, new PatternTopic(topicId));
	}

	private final Executor<Cache> memoryCacheExecutor=new Executor<Cache>() {

		@Override
		Cache get() {
			RequestAttributes attrs= RequestContextHolder.getRequestAttributes();

			if(attrs==null) {
				return null;
			}

			Cache cache=(Cache)attrs.getAttribute(memoryId, RequestAttributes.SCOPE_REQUEST);

			if(cache!=null) {
				return cache;
			}
			synchronized (MybatisRedisCache.this) {
				cache=new MemoryCache();
				attrs.setAttribute(memoryId, cache, RequestAttributes.SCOPE_REQUEST);
			}

			return cache;
		}

	};


	private abstract static class Executor<T>{


		public<R> R run(Function<T,R> fun) {
			T t=get();
			if(t==null) {
				return null;
			}
			return fun.apply(t);
		}

		abstract T get();

	}

	private class MemoryCache implements Cache{

		private final Map<Object, Object> map=Collections.synchronizedMap(new HashMap<>());
		@Override
		public Object removeObject(Object key) {
			return map.remove(key);
		}

		@Override
		public void putObject(Object key, Object value) {
			map.put(key, value);

		}

		@Override
		public int getSize() {
			return map.size();
		}

		@Override
		public Object getObject(Object key) {
			return map.get(key);
		}

		@Override
		public String getId() {
			return id;
		}

		@Override
		public void clear() {
			map.clear();

		}

		@Override
		protected void finalize() throws Throwable {
			super.finalize();
		}

	}



	public MybatisRedisCache(final String id) {
		log.info("RedisCache:{}", id);
		if (id == null) {
			throw new IllegalArgumentException("Cache instances require an ID");
		}
		this.id = id;
		this.topicId="TOPIC-"+id;
		
	}

	@Override
	public String getId() {
		return this.id;
	}

	@Override
	public int getSize() {
		
		Integer count=redisExecutor.run(redisTemplate-> Objects.requireNonNull(redisTemplate.boundHashOps(id).size()).intValue());
		if(count==null) {
			return 0;
		}
		return count;
	}

	@Override
	public void putObject(final Object key, final Object value) {
		log.info("putObject[{}]:{}", id, key);
		
		memoryCacheExecutor.run(cache->{
			cache.putObject(key, value);
			return null;
		});
		
		redisExecutor.run(redisTemplate->{
			redisTemplate.boundHashOps(id).put(key, value);
			if (timeout != null && redisTemplate.getExpire(key) == -1) {
				redisTemplate.expire(id, timeout, TimeUnit.SECONDS);
			}
			return null;
		});
		
	}

	@Override
	public Object getObject(final Object key) {
		log.info("getObject[{}]:{}", id, key);
		Object value=memoryCacheExecutor.run(cache->cache.getObject(key));
		if(value!=null) {
			return value;
		}
		return redisExecutor.run(redisTemplate->{
			log.info("getObject-UseRedis[{}]:{}", id, key);
			Object data= redisTemplate.boundHashOps(id).get(key);
			if(data==null) {
				return null;
			}
			memoryCacheExecutor.run(cache->{
				cache.putObject(key, data);
				return data;
			});
			return data;
		});
	}

	@Override
	public Object removeObject(final Object key) {
		log.info("removeObject[{}]:{}", id, key);
		memoryCacheExecutor.run(cache->cache.removeObject(key));
		return redisExecutor.run(redisTemplate->redisTemplate.boundHashOps(id).delete(key));
	}

	@Override
	public void clear() {
		log.info("clear[{}]", id);
		memoryCacheExecutor.run(cache->{
			cache.clear();
			return null;
		});
		redisExecutor.run(redisTemplate->{
			Boolean flag=redisTemplate.delete(id);
			log.info("delete cache:{}->{}",id,flag);
			return flag;
			
		});

	}

	@Override
	public ReadWriteLock getReadWriteLock() {
		return readWriteLock;
	}

	@Override
	public String toString() {
		return "Redis {" + id + "}";
	}

	public void setTimeout(Integer timeout) {
		this.timeout = timeout;
	}

	public static void main(String[] args) {
		CacheKey cacheKey = new CacheKey(new Object[] { "1234567", 123, 123L, new ArrayList<>() });
		System.out.println(JSON.toJSONString(cacheKey));
		System.out.println(cacheKey);

	}

}
